//  IDVI 1.0 source copyright 1996 Garth A. Dickie
//
//  This source is free for non-commercial use.  No warranty, etc.
//  Please acknowledge reuse by including the line:
//
//  "Based in part on IDVI 1.0 source copyright 1996 Garth A. Dickie"
//
//  in your documentation and source code.  For commercial use or
//  distribution, please contact the author.  Please also send
//  questions, comments, bug reports, or fixes.
//
//  Best Regards,
//  Garth A. Dickie
//  dickie@elastic.avid.com

package ibook.v10.idvi.dvi;

import java.io.*;
import java.util.*;

import ibook.v10.colors.Colors;
import ibook.v10.html.HTMLTag;
import ibook.v10.html.HTMLFormatException;
import ibook.v10.idvi.*;
import ibook.v10.idvi.io.*;
import ibook.v10.idvi.font.*;
import ibook.v10.parameter.HashtableParameterStub;
import ibook.v10.parameter.ParameterException;
import ibook.v10.parameter.ParameterMissingException;
import ibook.v10.parameter.ParameterProcessor;
import ibook.v10.parameter.ParameterStub;

// The clause 'implements DVIFormat' lets us refer to the constants
// defined in the class DVIFormat as though they were defined in this class.

final public class DVITokenizer implements DVIFormat {

    //  the following variables are used to return results to the parser:

    final private static int kTokenNothing          = -1;

    final public static int kTokenCharacter         = 0;
    final public static int kTokenRule              = 1;
    final public static int kTokenPSFile            = 2;
    final public static int kTokenBeginLinkAnchor   = 3;
    final public static int kTokenBeginNameAnchor   = 4;
    final public static int kTokenEndAnchor         = 5;
    final public static int kTokenBeginColor        = 6;
    final public static int kTokenEndColor          = 7;
    final public static int kTokenBeginApplet       = 8;
    final public static int kTokenEndApplet         = 9;
    final public static int kTokenParam             = 10;
    final public static int kTokenBeginPage         = 11;
    final public static int kTokenEndPage           = 12;
    final public static int kTokenPostamble         = 13;

    public int              token;

    public DVICharacter     character;
    public int              characterUnscaledX;
    public int              characterUnscaledY;

    public int              ruleUnscaledX;
    public int              ruleUnscaledY;
    public int              ruleUnscaledWidth;
    public int              ruleUnscaledHeight;

    public String           psFilePrefix;
    public int              psFileUnscaledX;
    public int              psFileUnscaledY;
    public int              psFileUnscaledWidth;
    public int              psFileUnscaledHeight;
    public Hashtable        psFileParameterHashtable;
    public ParameterProcessor psFileParameter;

    public ParameterStub    appletParameter;
    public String           appletCode;

    public String           paramName;
    public String           paramValue;

    public String           href;               // used for begin link anchor token
    public String           target;             // used for begin link anchor token
    public String           name;               // used for begin name anchor token
    public int              color;              // used for begin color token

    //  the constructor:

    private DVIInputStream  source;
    private DVIDocument     document;
    private MessageContext  context;
    private double          conversion;

    public DVITokenizer( DVIInputStream source, DVIDocument document, MessageContext context )
            throws IOException, DVIFormatException {
        this.source = source;
        this.document = document;
        this.context = context;

        conversion = document.getConversion( );

        advance( );
    }

    public void close( ) throws IOException {
        source.close( );
    }

    //  advance reads the next token from the input stream.  

    final public int advance( )
            throws IOException, DVIFormatException {

        token = kTokenNothing;
        while( token == kTokenNothing ) {

            int command = source.unsigned1( );

            if( command <= kHighCharacter ) {
                setCharacter( command );
            } else if( command >= kLowFont && command <= kHighFont ) {
                setFont( command - kLowFont );
            } else switch( command ) {
                case kCommandSET1:      setCharacter( source.unsigned1( ));                 break;
                case kCommandSETRULE:   setRule( source.unsigned4( ), source.unsigned4( )); break;
                case kCommandPUT1:      putCharacter( source.unsigned1( ));                 break;
                case kCommandPUTRULE:   putRule( source.unsigned4( ), source.unsigned4( )); break;
                case kCommandNOP:                                                           break;
                case kCommandBOP:       beginningOfPage( );
                                        source.skip( kPageHeaderLength );                   break;
                case kCommandEOP:       endOfPage( );                                       break;
                case kCommandPUSH:      push( );                                            break;
                case kCommandPOP:       pop( );                                             break;
                case kCommandRIGHT1:    xDelta( source.signed1( ));                         break;
                case kCommandRIGHT2:    xDelta( source.signed2( ));                         break;
                case kCommandRIGHT3:    xDelta( source.signed3( ));                         break;
                case kCommandRIGHT4:    xDelta( source.signed4( ));                         break;
                case kCommandW0:        xDeltaSave1( );                                     break;
                case kCommandW1:        xDeltaSave1( source.signed1( ));                    break;
                case kCommandW2:        xDeltaSave1( source.signed2( ));                    break;
                case kCommandW3:        xDeltaSave1( source.signed3( ));                    break;
                case kCommandW4:        xDeltaSave1( source.signed4( ));                    break;
                case kCommandX0:        xDeltaSave2( );                                     break;
                case kCommandX1:        xDeltaSave2( source.signed1( ));                    break;
                case kCommandX2:        xDeltaSave2( source.signed2( ));                    break;
                case kCommandX3:        xDeltaSave2( source.signed3( ));                    break;
                case kCommandX4:        xDeltaSave2( source.signed4( ));                    break;
                case kCommandDOWN1:     yDelta( source.signed1( ));                         break;
                case kCommandDOWN2:     yDelta( source.signed2( ));                         break;
                case kCommandDOWN3:     yDelta( source.signed3( ));                         break;
                case kCommandDOWN4:     yDelta( source.signed4( ));                         break;
                case kCommandY0:        yDeltaSave1( );                                     break;
                case kCommandY1:        yDeltaSave1( source.signed1( ));                    break;
                case kCommandY2:        yDeltaSave1( source.signed2( ));                    break;
                case kCommandY3:        yDeltaSave1( source.signed3( ));                    break;
                case kCommandY4:        yDeltaSave1( source.signed4( ));                    break;
                case kCommandZ0:        yDeltaSave2( );                                     break;
                case kCommandZ1:        yDeltaSave2( source.signed1( ));                    break;
                case kCommandZ2:        yDeltaSave2( source.signed2( ));                    break;
                case kCommandZ3:        yDeltaSave2( source.signed3( ));                    break;
                case kCommandZ4:        yDeltaSave2( source.signed4( ));                    break;
                case kCommandFNT1:      setFont( source.unsigned1( ));                      break;
                case kCommandFNT2:      setFont( source.unsigned2( ));                      break;
                case kCommandFNT3:      setFont( source.unsigned3( ));                      break;
                case kCommandFNT4:      setFont( source.unsigned4( ));                      break;
                case kCommandXXX1:      special( source.string( source.unsigned1( )));      break;
                case kCommandXXX2:      special( source.string( source.unsigned2( )));      break;
                case kCommandXXX3:      special( source.string( source.unsigned3( )));      break;
                case kCommandXXX4:      special( source.string( source.unsigned4( )));      break;
                case kCommandFNTDEF1:   defineFont( source.unsigned1( ), source );          break;
                case kCommandFNTDEF2:   defineFont( source.unsigned2( ), source );          break;
                case kCommandFNTDEF3:   defineFont( source.unsigned3( ), source );          break;
                case kCommandFNTDEF4:   defineFont( source.unsigned4( ), source );          break;
                case kCommandPOST:      postamble( );                                       break;

                default:
                    throw new DVIFormatException( "unexpected command " + command + "in %." );
            }
        }

        if( IDVI.debugTokenDelay != 0 )
            try {
                Thread.currentThread( ).sleep( IDVI.debugTokenDelay );
            } catch( InterruptedException e ) {
            }

        if( IDVI.debugTokenLog )
            System.out.println( toString( ));

        return token;
    }

    //  these are page state variables:

    private Stack           frameStack;
    private DVIFrame        frame;
    private DVIDocumentFont documentFont;
    private boolean         documentFontValid;

    //  and helper functions for the tokenizer:

    private void setCharacter( int characterNumber ) {
        if( documentFontValid ) {
            putCharacter( characterNumber );
            frame.x += ( int )( documentFont.conversion * ( double ) character.getTFMWidth( ));
        }
    }

    private void setRule( int height, int width ) {
        putRule( height, width );
        frame.x += ( int )( conversion * ( double ) width );
    }

    private void putCharacter( int characterNumber ) {
        if( documentFontValid ) {
            token = kTokenCharacter;

            character = documentFont.font.getCharacter( characterNumber );
            characterUnscaledX = frame.x + 0x8000 >> 16;
            characterUnscaledY = frame.y + 0x8000 >> 16;
        }
    }

    private void putRule( int height, int width ) {
        token = kTokenRule;

        ruleUnscaledX = frame.x + 0x8000 >> 16;
        ruleUnscaledY = frame.y + 0x8000 >> 16;
        ruleUnscaledWidth = ( int )( conversion * ( double ) width ) + 0x8000 >> 16;
        ruleUnscaledHeight = ( int )( conversion * ( double ) height ) + 0x8000 >> 16;

        if( ruleUnscaledWidth == 0 && width != 0 )
            ruleUnscaledWidth = 1;
        
        if( ruleUnscaledHeight == 0 && height != 0 )
            ruleUnscaledHeight = 1;
    }

    private void beginningOfPage( ) {
        token = kTokenBeginPage;

        frameStack = new Stack( );
        frame = new DVIFrame( document );
    }

    private void endOfPage( ) {
        token = kTokenEndPage;
    }

    private void push( ) {
        frameStack.push( frame );
        frame = new DVIFrame( frame );
    }

    private void pop( ) {
        frame = ( DVIFrame ) frameStack.pop( );
    }

    private void xDelta( int delta ) {
        frame.x += ( int )( conversion * ( double ) delta );
    }

    private void xDeltaSave1( ) {
        frame.x += frame.xDelta1;
    }

    private void xDeltaSave1( int delta ) {
        frame.xDelta1 = ( int )( conversion * ( double ) delta );
        frame.x += frame.xDelta1;
    }

    private void xDeltaSave2( ) {
        frame.x += frame.xDelta2;
    }

    private void xDeltaSave2( int delta ) {
        frame.xDelta2 = ( int )( conversion * ( double ) delta );
        frame.x += frame.xDelta2;
    }

    private void yDelta( int delta ) {
        frame.y += ( int )( conversion * ( double ) delta );
    }

    private void yDeltaSave1( ) {
        frame.y += frame.yDelta1;
    }

    private void yDeltaSave1( int delta ) {
        frame.yDelta1 = ( int )( conversion * ( double ) delta );
        frame.y += frame.yDelta1;
    }

    private void yDeltaSave2( ) {
        frame.y += frame.yDelta2;
    }

    private void yDeltaSave2( int delta ) {
        frame.yDelta2 = ( int )( conversion * ( double ) delta );
        frame.y += frame.yDelta2;
    }

    private void setFont( int number ) {
        documentFont = document.getFont( number );
        documentFontValid = documentFont.font != null;
    }

    private void special( String string ) {
        if( string.regionMatches( true, 0, kSpecialHTML, 0, kSpecialHTML.length( ))) {
            htmlSpecial( string.substring( kSpecialHTML.length( )));
        } else if( string.regionMatches( true, 0, kSpecialPSFile, 0, kSpecialPSFile.length( ))) {
            try {
                psFileSpecial( string );
            } catch( ParameterException e ) {
                if( IDVI.debugSpecials )
                    System.out.println( "psfile special: " + e.getMessage( ));
            }
        } else if( string.regionMatches( true, 0, kSpecialColor, 0, kSpecialColor.length( ))) {
            try {
                colorSpecial( string.substring( kSpecialColor.length( )));
            } catch( ParameterException e ) {
                if( IDVI.debugSpecials )
                    System.out.println( "color special: " + e.getMessage( ));
            }
        } else {
            if( IDVI.debugSpecials )
                System.out.println( "Unknown special: \"" + string + "\"" );
        }
    }

    private void defineFont( int number, DVIInputStream source ) throws IOException {
        int checksum = source.unsigned4( );
        int scale = source.unsigned4( );
        int design = source.unsigned4( );
        String name = source.string( source.unsigned1( ) + source.unsigned1( ));

        document.defineFont( number, checksum, scale, design, name, context );
    }

    private void postamble( ) {
        token = kTokenPostamble;
    }




    //  handling for different sorts of specials:

    private void htmlSpecial( String string ) {
        try {
            HTMLTag html = new HTMLTag( string );
            String tag = html.getTag( );

            if( tag.equalsIgnoreCase( kHTMLTagEndAnchor )) {

                token = kTokenEndAnchor;

            } else if( tag.equalsIgnoreCase( kHTMLTagBeginAnchor )) {

                href = html.getParameter( kHTMLParameterHREF );
                name = html.getParameter( kHTMLParameterName );
                target = html.getParameter( kHTMLParameterTarget );

                if( href != null )
                    token = kTokenBeginLinkAnchor;
                else if( name != null )
                    token = kTokenBeginNameAnchor;

            } else if( tag.equalsIgnoreCase( kHTMLTagBeginApplet )) {

                appletParameter = html;
                appletCode = html.getParameter( kHTMLParameterCode );

                if( appletCode != null )
                    token = kTokenBeginApplet;

            } else if( tag.equalsIgnoreCase( kHTMLTagParam )) {
                
                paramName = html.getParameter( kHTMLParameterName );
                paramValue = html.getParameter( kHTMLParameterValue );

                if( paramName != null && paramValue != null )
                    token = kTokenParam;

            } else if( tag.equalsIgnoreCase( kHTMLTagEndApplet )) {

                token = kTokenEndApplet;

            }
        } catch( HTMLFormatException e ) {
        } catch( IOException e ) {
        }

        if( token == kTokenNothing && IDVI.debugSpecials )
            System.out.println( "Unknown hypertex special: \"" + string + "\"" );
    }




    private void psFileSpecial( String string ) throws ParameterException {
        psFileParameterHashtable = psFileParseParameters( string );
        psFileParameter = new ParameterProcessor( new HashtableParameterStub( psFileParameterHashtable ));

        String imageFullName = psFileParameter.getString( kPSFileName );
        int psFilePrefixEnd = imageFullName.lastIndexOf( '.' );
        if( psFilePrefixEnd == -1 )
            psFilePrefix = imageFullName;
        else
            psFilePrefix = imageFullName.substring( 0, psFilePrefixEnd );

        int top    = psFileParameter.getInteger( kPSFileTop    );
        int left   = psFileParameter.getInteger( kPSFileLeft   ); 
        int bottom = psFileParameter.getInteger( kPSFileBottom ); 
        int right  = psFileParameter.getInteger( kPSFileRight  ); 

        int intForceWidth = 0;
        boolean hasForceWidth = false;
        boolean realForceWidth = false;
        double forceWidth = 0.0;
        if( psFileParameter.existsParameter( kPSFileForceWidth )) {
            intForceWidth = psFileParameter.getInteger( kPSFileForceWidth ); 
            hasForceWidth = true;
            realForceWidth = intForceWidth != 10 * ( right - left );
            forceWidth = ( double ) intForceWidth / 10.0;
        }

        int intForceHeight = 0;
        boolean hasForceHeight = false;
        boolean realForceHeight = false;
        double forceHeight = 0.0;
        if( psFileParameter.existsParameter( kPSFileForceHeight )) {
            intForceHeight = psFileParameter.getInteger( kPSFileForceHeight ); 
            hasForceHeight = true;
            realForceHeight = intForceHeight != 10 * ( bottom - top );
            forceHeight = ( double ) intForceHeight / 10.0;
        }

        double width  = right - left  ;
        double height = top   - bottom; // these are lower-left based postscript coordinates

        if( hasForceWidth && hasForceHeight && ( realForceWidth || realForceHeight )) {
            psFilePrefix += kPSFileWidthPrefix + intForceWidth + kPSFileHeightPrefix + intForceHeight;

            width  = forceWidth;
            height = forceHeight;
        } else if( hasForceWidth && realForceWidth ) {
            psFilePrefix += kPSFileWidthPrefix + intForceWidth;

            height *= forceWidth / width;
            width = forceWidth;
        } else if( hasForceHeight && realForceHeight ) {
            psFilePrefix += kPSFileHeightPrefix + intForceHeight;

            width *= forceHeight / height;
            height = forceHeight;
        }

        int dpi = document.getDPI( );

        psFileUnscaledX      = frame.x + 0x8000 >> 16;
        psFileUnscaledY      = frame.y + 0x8000 >> 16;
        psFileUnscaledWidth  = ( int ) Math.ceil(( width + 1.0 ) * ( double ) dpi / kPSFileDPI );
        psFileUnscaledHeight = ( int ) Math.ceil(( height + 1.0 ) * ( double ) dpi / kPSFileDPI );

        token = kTokenPSFile;
    }

    final private static int kPSFileParametersInitialSize = 11;

    private Hashtable psFileParseParameters( String string ) {
        Hashtable result = new Hashtable( kPSFileParametersInitialSize );

        int valueEnd = string.length( );
        while( valueEnd > 0 ) {
            int keyStart = 1 + string.lastIndexOf( ' ', valueEnd - 1 );
            if( keyStart != valueEnd ) {
                int keyEnd = string.indexOf( '=', keyStart );
                if( keyEnd == -1 || keyEnd >= valueEnd )
                    result.put(
                        string.substring( keyStart, valueEnd ).toLowerCase( ),
                        "" );
                else {
                    int valueStart = keyEnd + 1;
                    result.put(
                        string.substring( keyStart, keyEnd ).toLowerCase( ),
                        string.substring( valueStart, valueEnd ));
                }
            }

            valueEnd = keyStart - 1;
        }
        return result;
    }




    private void colorSpecial( String string ) throws ParameterException {
        try {
            StringTokenizer stringTokenizer = new StringTokenizer( string );
            String stringToken = stringTokenizer.nextToken( );

            if( stringToken.equals( kColorPush )) {
                stringToken = stringTokenizer.nextToken( );

                if( stringToken.equals( kColorCMYK )) {
                    try {
                        double c = new Double( stringTokenizer.nextToken( )).doubleValue( );
                        double m = new Double( stringTokenizer.nextToken( )).doubleValue( );
                        double y = new Double( stringTokenizer.nextToken( )).doubleValue( );
                        double k = new Double( stringTokenizer.nextToken( )).doubleValue( );

                        color = Colors.getCMYKColor( c, m, y, k );
                    } catch( NumberFormatException e ) {
                        throw new ParameterException( "", "bad value in \"" + string + "\"" );
                    }
                } else if( stringToken.equals( kColorRGB )) {
                    try {
                        double r = new Double( stringTokenizer.nextToken( )).doubleValue( );
                        double g = new Double( stringTokenizer.nextToken( )).doubleValue( );
                        double b = new Double( stringTokenizer.nextToken( )).doubleValue( );

                        color = Colors.getRGBColor( r, g, b );
                    } catch( NumberFormatException e ) {
                        throw new ParameterException( "", "bad value in \"" + string + "\"" );
                    }
                } else {
                    color = Colors.getNamedColor( stringToken );
                }

                token = kTokenBeginColor;
            } else if( stringToken.equals( kColorPop )) {
                token = kTokenEndColor;
            } else {
                if( IDVI.debugSpecials )
                    System.out.println( "Color special \"color " + string + "\" is not color push or color pop" );
            }
        } catch( NoSuchElementException e ) {
            throw new ParameterMissingException( string );
        }
    }

    public String toString( ) {
        String result;

        switch( token ) {
            case kTokenCharacter:
                result = "character, x = " + characterUnscaledX +
                        ", y = " + characterUnscaledY;
                break;
            case kTokenRule:
                result = "rule, x = " + ruleUnscaledX +
                        ", y = " + ruleUnscaledY + 
                        ", width = " + ruleUnscaledWidth +
                        ", height = " + ruleUnscaledHeight;
                break;
            case kTokenPSFile:
                result = "image, prefix = \"" + psFilePrefix +
                        "\", x = " + psFileUnscaledX +
                        ", y = " + psFileUnscaledY +
                        ", width = " + psFileUnscaledWidth + 
                        ", height = " + psFileUnscaledHeight;
                break;
            case kTokenBeginLinkAnchor:
                result = "anchor, href = \"" + href + "\"";
                break;
            case kTokenBeginNameAnchor:
                result = "anchor, name = \"" + name + "\"";
                break;
            case kTokenEndAnchor:
                result = "end anchor";
                break;
            case kTokenBeginColor:
                result = "begin color, color = " + color;
                break;
            case kTokenEndColor:
                result = "end color";
                break;
            case kTokenBeginPage:
                result = "begin page";
                break;
            case kTokenEndPage:
                result = "end page";
                break;
            case kTokenPostamble:
                result = "end document";
                break;
            default:
                result = "unknown token, number " + token;
                break;
        }

        return result;
    }
}

class DVIFrame {
    final private static double kTopMargin = 1.0;
    final private static double kLeftMargin = 1.0;

    int     x;
    int     xDelta1;
    int     xDelta2;

    int     y;
    int     yDelta1;
    int     yDelta2;

    public DVIFrame( DVIDocument document ) {
        double dpi = ( double ) document.getDPI( );

        x = ( int )( kTopMargin * dpi * 65536.0 + 0.5 );
        y = ( int )( kLeftMargin * dpi * 65536.0 + 0.5 );
    }

    public DVIFrame( DVIFrame copy ) {
        x       = copy.x;
        xDelta1 = copy.xDelta1;
        xDelta2 = copy.xDelta2;

        y       = copy.y;
        yDelta1 = copy.yDelta1;
        yDelta2 = copy.yDelta2;
    }
}
